Skip to content

Remove unnecessary .txt extensions from example and source files#1

Merged
PhysShell merged 5 commits into
mainfrom
claude/remove-txt-extensions-9xdocy
Jun 14, 2026
Merged

Remove unnecessary .txt extensions from example and source files#1
PhysShell merged 5 commits into
mainfrom
claude/remove-txt-extensions-9xdocy

Conversation

@PhysShell

Copy link
Copy Markdown
Owner

Rename the four example/source files to their real extensions so they
match the names already used in the README and file headers:
Program.cs.txt -> Program.cs
buffer.own.txt -> buffer.own
ok_pool.own.txt -> ok_pool.own
bad_leak_branch.own.txt -> bad_leak_branch.own

claude added 5 commits June 13, 2026 16:58
Rename the four example/source files to their real extensions so they
match the names already used in the README and file headers:
  Program.cs.txt          -> Program.cs
  buffer.own.txt          -> buffer.own
  ok_pool.own.txt         -> ok_pool.own
  bad_leak_branch.own.txt -> bad_leak_branch.own
The modules used relative imports (.parser, .cfg, ...) and run_tests.py
imported the 'ownlang' package, but everything sat flat in the repo
root, so 'python tests/run_tests.py' failed with ModuleNotFoundError.

Move files into the layout already documented in the README:
  ownlang/    <- the 8 .py modules + new __init__.py
  tests/      <- run_tests.py
  examples/                 <- ok_pool.own, bad_leak_branch.own
  examples/golden_arraypool <- buffer.own, Program.cs

Now passing: 42/42 analysis, 11/11 codegen, golden PASS; and the
documented CLI (python -m ownlang check/emit/cfg) works.
Ignore __pycache__/ and *.pyc (generated when running the test suite)
plus the .NET bin/ and obj/ output from the golden_arraypool demo, so
build artifacts stay out of the repo.
README's lead commands pointed at examples/ok_extern_calls.own and
examples/bad_maybe_release.own, which never existed as files (they were
only inline test cases). Add them as standalone .own files, verified
with the checker here:
  ok_extern_calls.own   -> checks clean (extern borrow/borrow_mut/consume)
  bad_maybe_release.own -> fires OWN009 + OWN001, cfg/emit behave as documented

For demo.csproj (referenced but never shipped, and unverifiable without a
.NET SDK in this sandbox) fix the README instead of adding an untested
project file: show how to wrap Program.cs in a console project, and drop
the demo.csproj entry from the structure tree.
Two blocking issues from code review, both reproduced before fixing:

1. Soundness hole: returning an owner while it is borrowed checked clean.
   'return b' is an escape/consume of an owned resource, so under a live
   loan it must fail like move/consume do. The Return handler validated
   moved/released state but never the active loans on the returned owner.
   Now it fires OWN007 (mirrors move_while_borrowed/consume_while_borrowed).
   Added regression test return_while_borrowed; updated the OWN007 row in
   the README code table to list return alongside move/consume.

2. README quickstart was broken: 'cd ownlang' steps into the package dir,
   after which 'python -m ownlang' fails (No module named ownlang) and the
   examples/ paths no longer resolve. Replaced with a note to run from the
   repo root.

Suite: 43/43 analysis (was 42 + new case), 11/11 codegen, golden PASS.
PhysShell pushed a commit that referenced this pull request Jun 14, 2026
PR #2 was authored with package-style relative imports (from . import ...)
but committed as flat files, so it never actually ran. Re-home it onto the
package layout established by PR #1: modules into ownlang/, buffers.py and
report.py alongside, the test runner + its buffer_scratch_program.cs.txt
fixture into tests/, example programs into examples/. Merge .gitignore and
splice PR #1's README quickstart fix into PR #2's README. No logic changes.
PhysShell pushed a commit that referenced this pull request Jun 14, 2026
PR #2's rewritten Return handling set the owner to ESCAPED without checking
for a live loan, so `borrow b as s { return b; }` returned a resource out
from under an outstanding borrow with no diagnostic. PR #1 caught this on the
original analysis; re-apply it, adapted to PR #2's buffer-aware Return block:
returning an owner needs Own permission, so a live loan is OWN007 (as for move).
@PhysShell
PhysShell merged commit 9c08466 into main Jun 14, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #3, which landed all three PRs as one linear integration on main (merged in 01da03d). This PR's restructure, examples, .gitignore, README fixes, and the return-under-borrow soundness fix are all included. Closing.


Generated by Claude Code

PhysShell pushed a commit that referenced this pull request Jun 14, 2026
The core ownership checker already catches the main class of WPF lifetime
leaks once a ViewModel is modelled as a scope (constructor = scope start,
Dispose = scope end): a subscription/timer that is never disposed is OWN001,
touched after dispose is OWN002. Slice #1 makes that usable without inventing
WPF-specific codes:

- resource declarations gain an optional, contextual 'kind "..."' attribute
  (no new reserved word). It is domain-neutral metadata threaded onto the owning
  Symbol and surfaced on diagnostics as a '[resource: <kind>]' suffix -- the seam
  a later WPF profile / Roslyn front-end keys off, with the core staying generic.
- corpus/wpf/: two self-checking real-pattern cases (zombie ViewModel -> OWN001,
  handler-use-after-dispose -> OWN002) with before/after C#, case.own, expected
  codes and honesty notes; tests/test_wpf.py pins both the codes AND that the
  kind tag reaches rendered output.
- docs/lifetimes.md captures the full module plan (lifetime regions, OWN-WPF
  catalogue, slice boundaries); README gains a business-application section.
- bonus: assert_never now also guards lower_let's rhs dispatch; the strict gate
  caught a real 'kind' name collision in analysis.step during this work.

Gate + suite green: analysis 123/123, codegen 23/23, fuzz, gallery 10/10,
corpus 2/2, wpf 2/2.
PhysShell pushed a commit that referenced this pull request Jun 14, 2026
The genuinely-new analysis on top of the ownership core: it reasons about region
escape (the WPF 'zombie ViewModel promoted to App lifetime' theorem), not just
release within a scope.

Language:
- lifetime declarations with a strict order: 'lifetime Window < App;'
  (transitive; cycles -> OWN036, undefined refs -> OWN030, redef -> OWN031).
- lifetime annotations on the object a function sets up and on its service
  params: 'fn VM(bus: EventBus lifetime App) lifetime ViewModel'.
- a strong-capture statement: 'subscribe self to SOURCE;'
  ('self'/'to' contextual; only 'lifetime'/'subscribe' are reserved; '<' lexed).

Analysis (ownlang/lifetimes.py, check_lifetimes):
- region escape OWN014: if SOURCE strictly outlives self, the strong capture
  promotes self to SOURCE's lifetime and it leaks. The *ordering* is what makes
  it a leak -- a same/shorter-lived source is clean. Per fork B the code is
  domain-neutral (OWN014 'escapes to a longer-lived region'), not WPF-branded.
- the mitigation (a disposable token released on close) is the slice-#1
  acquire/release pattern, so both halves of the theorem now exist.

Wiring/tests:
- CLI 'check' runs check_lifetimes; the new Subscribe stmt is a no-op for the
  loans/permissions flow and a schematic emit in codegen (both assert_never
  dispatchers updated; lower_let rhs got assert_never too).
- corpus/wpf/viewmodel-escapes-to-app (before/after C# + notes); test_wpf made
  tolerant of non-kinded cases. tests/test_lifetimes.py: 10 region cases.
- docs/lifetimes.md + README updated to mark slice #2 built.

Gate + suite green: mypy --strict, ruff, analysis 123/123, codegen 23/23, fuzz,
gallery 10/10, corpus 2/2, wpf 3/3, lifetimes 10/10.
PhysShell pushed a commit that referenced this pull request Jun 16, 2026
#1 --severity {error,warning} (default error): threaded through the core
renderer (Finding.render/_github/_msbuild + render_finding), the ownir CLI
(--severity, validated; only valid for ownir), own-check.sh, and the
composite Action's new 'severity' input. A presentation choice — the finding
is still the core's verdict — so a build can show leaks as advisory warnings
instead of failing. Resolves P-013 open question #1.

#2 scripts/own-check.ps1: a PowerShell twin of own-check.sh so Windows/VS
users without bash get the same one-command pipeline (same flags/output).

Docs: how-to §2B now uses --severity warning (drops the sed hack) and §4
points at own-check.ps1; P-013 scope/open-questions updated. CI: new step
asserts --severity warning emits warning-level (not error) MSBuild lines.

Verified locally: ownir 28/28, ruff + mypy --strict clean, severity honored
in github/msbuild/human, bad/misapplied severity returns 2.

https://claude.ai/code/session_01BE8MeHTtFcrTBjtMAQ7Yc6
PhysShell added a commit that referenced this pull request Jun 16, 2026
…egy-gs41zk

level-2: add --severity flag + PowerShell own-check.ps1

#1 --severity {error,warning} (default error): threaded through the core
renderer (Finding.render/_github/_msbuild + render_finding), the ownir CLI
(--severity, validated; only valid for ownir), own-check.sh, and the
composite Action's new 'severity' input. A presentation choice — the finding
is still the core's verdict — so a build can show leaks as advisory warnings
instead of failing. Resolves P-013 open question #1.

#2 scripts/own-check.ps1: a PowerShell twin of own-check.sh so Windows/VS
users without bash get the same one-command pipeline (same flags/output).
PhysShell pushed a commit that referenced this pull request Jun 18, 2026
… re-validation

Leak #4 in the SystemEvents fixture: a FileStream that IS disposed, but the
Dispose() sits inside the try after a may-throw call (WriteByte), so it's
skipped on the exceptional path. This is CodeQL's cs/dispose-not-called-on-throw
(cs/local-not-disposed also models it). Own.NET used to miss it — disposed
somewhere looked balanced — until the exception-edge model (77b2edd) inserted a
throw edge before each may-throw statement in a try.

Re-running the oracle on this fixture should land #4 in "Agree" with CodeQL,
joining #2/#3, with #1 (subscription) staying Own.NET-only. Bumps the oracle
sentinel to trigger the push-run.
PhysShell added a commit that referenced this pull request Jun 21, 2026
… Q#1) (#66)

DI001/DI002/DI003 keep their primary anchor at the registration site but now also name
the CONSUMING CONSTRUCTOR — where the captive dependency is injected — so the developer
is pointed at the code, not just the wiring. P-006 open question #1 ("anchor at the
registration line, the consuming constructor, or both?") answered: both, with the path
shown, like OWN014.

- Extractor records each implementation's ctor location (the widest public ctor's
  declaration, or the class declaration for a C# 12 primary / implicit ctor) and the
  implementation type into ctor_file / ctor_line / ctor_type on the service fact.
- Core appends ` [consumed by the '<impl>' constructor at <file>:<line>]` to the captive
  message (every surface) and carries it as a structured Finding.related triple, emitted
  as a SARIF relatedLocation — clickable + cross-file (registration in Startup.cs, ctor in
  EmailSender.cs). The owner named is the IMPLEMENTATION type, never the (ctor-less) service
  interface for an interface registration (Codex). Degrades cleanly (no tail, no related)
  when the location is unknown, so hand-authored facts / an older extractor still produce
  the prior form. ownir load() validates ctor_file/ctor_line/ctor_type.

Pinned end-to-end by DiCaptiveSample.cs (explicit-ctor EmailSender:25, primary-ctor
PrimaryCtorService:33, ConnectionWarmer:50, WeakCache:57) in the wpf-extractor CI job, the
cross-file + interface-registration cases by tests, and the SARIF related location by
tests/test_ownir.py. DI004's consumer is a resolution call site (not a ctor) — its anchor
is the sibling follow-up. ruff + mypy --strict clean, ownir 113/113; DI counts unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
PhysShell added a commit that referenced this pull request Jun 22, 2026
fix(extractor): resolve-aware field disposability (mined ImageSharp FP #1/4)
PhysShell pushed a commit that referenced this pull request Jun 23, 2026
…t rebinds (Codex P2 + CodeRabbit)

The first cut keyed the alias map by identifier STRING, scanned class-wide, and
gated reassignment only on `=` expressions. Three holes, all flagged in review:

- CodeRabbit (major) + Codex #2 — class-wide name keys conflate same-named locals
  across methods: an unrelated `c.Dispose()` in one method could credit a field
  aliased by a `c` in another (FN), and two aliases reusing a name overwrite each
  other. Now keyed by the LOCAL SYMBOL (model.GetDeclaredSymbol(decl)), and the
  dispose scan resolves the receiver symbol — so each alias is scope-exact.
- Codex #1 — an alias rebound through a `ref`/`out` argument (`Swap(ref a)`) was
  not counted as reassigned, so a disposed-after-rebind alias wrongly credited the
  field (FN). The reassignment set now includes ref/out argument locals.
- Iterating declarators by symbol (is ILocalSymbol) also drops non-local
  declarators from the map (locals-only, as intended).

Direct field disposals are unchanged: a receiver that is not an alias local falls
back to FieldName, exactly as before.

Regression controls added: RefReboundAliasLeaks (`Swap(ref a)` then dispose ->
_refRebound still leaks) and ScopedAliasNameLeak (an unrelated same-named `c`
disposed in another method must not credit _scopedLeak -> it still leaks). CI
asserts both, alongside the existing aliased-silent and never-disposed/`=`-rebound
controls.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
PhysShell pushed a commit that referenced this pull request Jun 23, 2026
…ped); drop the SemaphoreSlim exemption

Scopes this PR to the uncontentious fix and addresses the review.

#1 `.Close()` releases a field — the field-disposable `disposed` scan credited a
   field only on Dispose()/DisposeAsync(), but the LOCAL detector (DisposesLocal and
   the flow detector) has always accepted Dispose/Close/DisposeAsync. That asymmetry
   flagged a field cleaned up by Close() as a leak. Mirror the local set on both the
   direct and null-conditional field-release checks. Mined: Npgsql ReplicationConnection
   releases its NpgsqlConnection field via `await _npgsqlConnection.Close(async: true)`.

   Receiver-scoped (Codex/CodeRabbit): credit only THIS instance's field or a validated
   alias via ThisFieldName, so `other._conn.Close()` on ANOTHER instance of the SAME
   class cannot mark this object's `_conn` released. (A field-symbol ContainingType
   check would NOT catch this — same class, same ContainingType — so the receiver is
   keyed syntactically; this also tightens the pre-existing Dispose path.)

Dropped: the SemaphoreSlim dispose-optional change. Putting SemaphoreSlim in the
shared IsDisposeOptional also affected the flow-locals detector and tripped the
deliberate `semLeak` control (a prior ShareX fix explicitly decided SemaphoreSlim
stays tracked for method-bounded locals, since reading AvailableWaitHandle allocates
a handle Dispose must release — Codex flagged the same). A sound, field-scoped +
AvailableWaitHandle-gated version is a separate decision, deferred.

Regression sample CloseReleaseSample.cs: ReleasesViaClose._closedConn / _closedConnQ
(direct + null-conditional Close) silent; controls still warn — LeaksUnclosed._leakedConn
(never closed) and ClosesOtherInstanceField._xconn (closed only on ANOTHER instance).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
PhysShell pushed a commit that referenced this pull request Jun 27, 2026
…line

Live NLog oracle re-run with the four WaitForDispose timer entries disabled
confirms the CallReleasesReceiver fix cleared them at the source: leak-class
own-only total 8 -> 4, and the four timers (AsyncTaskTarget _taskTimeoutTimer/
_lazyWriterTimer, AsyncTargetWrapper _lazyWriterTimer, BufferingTargetWrapper
_flushTimer) appear in neither own-only nor the baselined section. The remaining
NLog own-only is just the three benign-but-real undisposed fields (XmlParser
_xmlSource, FileTarget _reusable*Stream), with only TimeoutContinuation
_timeoutTimer baselined (its Interlocked.Exchange ref-alias is out of scope).

- Delete the four now-redundant baseline entries (baseline: 13 -> 9 rules).
- Document the fix landed in oracle-known-fps.md (root-cause #1 mostly-fixed).
- Restore the dev scaffolding: oracle-target.txt back to the systemevents-console
  fixture, remove the branch from the oracle.yml push filter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019846YSZ35c7CdkWQ1qX5gm
PhysShell pushed a commit that referenced this pull request Jun 28, 2026
…+ no-op-dispose BCL wrappers

Two precision fixes for the remaining open root-causes in oracle-known-fps.md.

Tail 1 (root-cause #1, custom dispose-sink): release a tracked local used as
the resource of a `using (existingLocal) { ... }` statement. The --flow-locals
lowering already handled the `using (var x = ...)` declaration form and the
MemoryPool owner statement form, but not the expression form over an
already-acquired local (`var timer = new ...; using (timer) { ... }`) — it left
the local looking live at method exit, a spurious OWN001. Now threads a
scope-exit release onto the body's normal completion and its return/throw exits
(no acquire — the local was acquired at its `new`). Clears protobuf-net's
assorted/ Silverlight Page.xaml.cs `timer` (baseline entry marked fix-landed,
pending a live-oracle re-run to confirm-and-delete).

Tail 2 (root-cause #2, no-op Dispose): extend the StringWriter/StringReader
field exemption to BCL pass-through wrappers. A StreamReader/StreamWriter/
BinaryReader/BinaryWriter field whose every construction wraps an in-memory
backing (MemoryStream/StringWriter/StringReader) cascades disposal only to
managed memory, so it is dispose-optional. Closed allowlist of those four
adapters (GZipStream/CryptoStream own native/extra resource — stay flagged);
a path building `new StreamReader(path)` (a real handle) keeps the field
flagged. Does NOT clear Newtonsoft's _textWriter (a third-party JsonTextWriter
wrapper whose Dispose we can't prove is pass-through without body analysis —
the soundness wall); that stays baselined, with the reasoning recorded so it
isn't unsoundly suppressed.

Corpus fixtures: local-dispose-via-using-statement, field-noop-dispose-wrapper
(each: before.cs caught / after.cs clean, proving the exemption stays narrow).
Docs: new no-op-dispose-wrapper.md; oracle-known-fps.md root-causes #1/#2 and
baseline annotations updated. Selftests: oracle 31/31, corpus 29/29,
validate_contrib 16/16, ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019846YSZ35c7CdkWQ1qX5gm
PhysShell pushed a commit that referenced this pull request Jun 28, 2026
…dev config

Ran the cross-tool oracle on protobuf-net (via the push-trigger dev fallback;
the token can't workflow_dispatch) to confirm whether the using-statement
local-release fix clears the assorted/ Silverlight Page.xaml.cs `timer` FP.

Result: NOT cleared — the finding is still emitted and still baselined. Re-triage
of the real source shows the prior baseline reason was a misdiagnosis read from
COMMENTED-OUT code: the only `using (timer)` block is commented out, and `timer`
is a Stopwatch (RunTest) / a DispatcherTimer local (btnRunTest_Click) — neither
is IDisposable. So the using-fix does not apply; the entry stays baselined as a
non-product sample FP, and the real open question is why a non-IDisposable
timer-shaped local is flagged at all (a precision follow-up needing a local repro).

- Corrected the baseline entry reason and oracle-known-fps.md (root-cause #1 +
  the per-finding row).
- Reverted the dev-only oracle config (oracle.yml push branch + oracle-target.txt
  back to the systemevents-console fixture).

Selftests: oracle 31/31, corpus 30/30.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019846YSZ35c7CdkWQ1qX5gm
PhysShell pushed a commit that referenced this pull request Jun 28, 2026
Cross-reference the tracked precision follow-up (non-IDisposable timer local
flagged OWN001) from the baseline entry and oracle-known-fps.md root-cause #1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019846YSZ35c7CdkWQ1qX5gm
PhysShell pushed a commit that referenced this pull request Jun 30, 2026
…pwatch), not an FP

Cracked the mechanism behind issue #161 from the core side: the finding renders
via the flow-locals path (ownir.py rkind=="flow-local"), whose extractor
candidate gate REQUIRES ImplementsIDisposable. The raw protobuf source shows the
file has no `using System.Diagnostics;` — it has `using Nuxleus.Performance;` and
uses `Stopwatch.UnitPrecision` / `UnitPrecision.NANOSECONDS` / `timer.Scope =
() => …`. So `Stopwatch` is `Nuxleus.Performance.Stopwatch`, a custom IDisposable
scope-timer (you can only `using` an IDisposable), NOT System.Diagnostics.Stopwatch.

`Stopwatch timer = new Stopwatch()` is genuinely never disposed — the disposing
`using (timer) { … }` block is commented out — so the OWN001 is a REAL leak the
extractor correctly flagged. It stays baselined as NON-PRODUCT sample (assorted/
Silverlight demo), like the NetTranscoder `sync` entry — reclassified from FP to
true-but-benign-sample. The earlier "missed using" and "non-IDisposable Stopwatch
FP" readings were both wrong; issue #161 is closed as invalid.

Corrected the baseline entry reason, oracle-known-fps.md root-cause #1, the
per-finding verdict row, and the disposition summary (Baselined FP 6→5,
true-but-benign sample 2→3). oracle selftest 31/31.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019846YSZ35c7CdkWQ1qX5gm
PhysShell pushed a commit that referenced this pull request Jul 3, 2026
Both reviewers converged; all corrections are factual/design refinements
(no forks):

- Crate DAG: own-codegen is a SIBLING of own-diagnostics (both consume
  own-cfg/own-analysis), not chained through it. Codegen is
  verdict-independent (AST/CFG-driven, matching Python codegen.generate),
  so the old own-diagnostics -> own-codegen edge would have forced
  diagnostics to re-export solver internals. Added a fitness function
  locking codegen !-> diagnostics (and !-> analysis for now). (Codex P2,
  CodeRabbit #1)
- Oracle: do NOT reuse scripts/oracle_compare.py as the parity oracle —
  it is cross-tool fuzzy (leak-only, +-N line tolerance, coarse severity)
  and would mask off-by-one/label/subject/exit-status divergences. Spec a
  new exact harness over status+stdout+stderr+SARIF/JSON, with an
  exit/crash gate first, exact set-equality incl. evidence label text,
  and intra-tie ordering. (Codex P1, CodeRabbit #4)
- CFG seam does not exist yet: python cfg prints human text, not JSON.
  Add+freeze a canonical cfg --format json before the ratchet uses that
  seam; added as migration step 0. (Codex P2)
- State: arena+CoW likely wins this procedural workload; bench largest
  real function, wall-clock + RSS. Prior art: clippy lint-pass registry,
  prusti-viper encoding boundary. Repo-layout revisit trigger. (CodeRabbit
  #2/#3/#5/#6)
PhysShell pushed a commit that referenced this pull request Jul 4, 2026
Closes 3 of 4 open questions with sourced findings; #1 and #3 retain a
5-minute empirical residual (run on real Linux), not a design blocker.

- gVisor x toolchain: LIKELY OK (277/351 syscalls; runtimes regression-
  tested). Residual: run gate.toml under runsc, watch for "Bad system call".
  Known gaps flagged (io_uring off, iptables partial, no KVM-in-sandbox).
- Kata 2026: CLOSED — ~100-300ms boot, ~130-200MB/pod, needs KVM, no native
  egress, guest-kernel patch-currency required. Its edge is containerd/K8s
  drop-in; with no K8s here it adds overhead without advantage over direct
  Firecracker (L1) or Sandlock (L2). Fills the deep-research Kata gap.
- QUIC/HTTP3 egress: CLOSED default — blanket UDP block suffices; CLI agent
  tools are TCP-first and fall back cleanly. Don't build MITM-CA/L7 proxy
  until a hard QUIC dependency appears. Residual: confirm nothing hangs.
- TCO self-host vs managed: CLOSED — both cheap in $ at N=1; the fork is
  purpose/trust/offline, not cost. For a for-the-soul self-use project,
  build self-host; the L2 MVP is days not weeks. E2B is benchmark, not target.

Also fixes the §6 trigger-table (an earlier note blockquote split the table
mid-rows).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LMA4JwvgPBCZuEbAiG5LJL
PhysShell added a commit that referenced this pull request Jul 14, 2026
… object

"Who holds this object" is ill-posed for an object reachable from many roots: there are
as many answers as there are paths, and the shortest is an arbitrary pick. Reporting one
of them was the tool's real weakness — the first version happily pointed at a prototype
held by the stack while the leak was eight hops away through a delegate's invocation list.

`roots` now samples the retained instances (--sample, default 200), computes every one's
shortest path in a SINGLE BFS (breadth-first from the whole root set gives each node its
shortest path for free), and reports the paths as a RANKED HISTOGRAM. On the live target
this finds the leak on its own:

  #1  25/50 (50.0%) — via [static-event], 7 hops
      System.Object[]
      BrokerDataClasses.Property.KernelProperty
      BrokerDataClasses.Property.GBProperty                (.fGBProperty)
      System.ComponentModel.PropertyChangedEventHandler    (.PropertyChanged)
      System.Object[]                                      (._invocationList)
      System.ComponentModel.PropertyChangedEventHandler
      BrokerDataClasses.GTD                                (._target)

  #4  1/50 (2.0%) — via [stack] — noise, and labelled as such

Parent pointers are stored as addresses, not labels; type and field names are resolved
only for the sampled paths. Carrying a label per node would cost hundreds of MB on a
4M-object heap.

The run also exposes the remaining limit, and the README now says so plainly: the
histogram partitions by SHORTEST path, so an object held by two references at once is
attributed to the nearer one. Here 50% of the GTDs land under the static event and 44%
under a static List<Object> — most likely many are held by BOTH, and detaching the event
alone would not free them. "Which single reference, if cut, frees this object" is a
different, well-posed question with a standard answer this tool does not implement: a
dominator tree with retained sizes (Lengauer-Tarjan / Cooper-Harvey-Kennedy — what
Eclipse MAT and dotMemory are built on). That is the honest next step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PjQnE1FDucd6vBVQbFswiE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants